import json
import re
import uuid

import redis
from redis.client import Redis

mconn = Redis(host="192.168.35.110", port=6379, db=0, decode_responses=True)

STOP_WORDS = set('able about the a as at to'.split())
WORD_RE = re.compile("[a-z']{2,}")


def tokenize(content):
    words = set()
    for match in WORD_RE.finditer(content.lower()):
        word = match.group().strip("’")
        if len(word) >= 2:
            words.add(word)
    return words - STOP_WORDS


def index_document(conn, docid, content):
    words = tokenize(content)
    pipe = conn.pipeline(True)
    for word in words:
        pipe.sadd('idx:' + word, docid)
    return len(pipe.execute())


def _set_common(conn, method, names, ttl=30, execute=True):
    id = str(uuid.uuid4())
    pipeline = conn.pipeline(True) if execute else conn
    names = ['idx:' + name for name in names]
    getattr(pipeline, method)('idx:' + id, *names)
    pipeline.expire('idx:' + id, ttl)
    if execute:
        pipeline.execute()
    return id


def intersect(conn, items, ttl=30, _execute=True):
    return _set_common(conn, 'sinterstore', items, ttl, _execute)


def union(conn, items, ttl=30, _execute=True):
    return _set_common(conn, 'sunionstore', items, ttl, _execute)


def difference(conn, items, ttl=30, _execute=True):
    return _set_common(conn, 'sdiffstore', items, ttl, _execute)


QUERY_RE = re.compile("[+-]?[a-z']{2,}")


def parse(query):
    unwanted = set()
    all = []
    current = set()
    for match in QUERY_RE.finditer(query.lower()):
        word = match.group()
        prefix = word[:1]
        if prefix in '+-':
            word = word[1:]
        else:
            prefix = None
        word = word.strip("'")
        if len(word) < 2 or word in STOP_WORDS:
            continue
        if prefix == '-':
            unwanted.add(word)
            continue
        if current and not prefix:
            all.append(list(current))
            current = set()
        current.add(word)

    if current:
        all.append(list(current))
    return all, list(unwanted)


def parse_and_search(conn, query, ttl=30):
    all, unwanted = parse(query)
    if not all:
        return None
    to_intersect = []
    intersect_result = []
    for syn in all:
        if len(syn) > 1:
            to_intersect.append(union(conn, syn, ttl=ttl))
        else:
            intersect_result = to_intersect[0]
    if unwanted:
        unwanted.insert(0, intersect_result)
        return difference(conn, unwanted, ttl=ttl)
    return intersect_result


def search_and_sort(conn, query, id=None, ttl=300, sort="-updated", start=0, num=20):
    desc = sort.startswith('-')
    sort = sort.lstrip('-')
    by = "kb:doc:*->" + sort
    alpha = sort not in ('updated', 'created', 'id')
    if id and not conn.expire(id, ttl):
        id = None

    if not id:
        id = parse_and_search(conn, query, ttl=ttl)

    pipe = conn.pipeline(True)
    pipe.scard('idx:' + id)
    pipe.sort('idx:' + id, by=by, alpha=alpha, desc=desc, start=start, num=num)
    results = pipe.execute()
    return results[0], results[1], id


def search_and_zsort(conn, query, id=None, ttl=300, update=1, vote=0, start=0, num=20, desc=True):
    if id and not conn.expire(id, ttl):
        id = None
    if not id:
        id = parse_and_search(conn, query, ttl=ttl)
        scored_search = {
            id: 0,
            'sort:update': update,
            'sort:votes': vote
        }
        id = zintersect(conn, scored_search, ttl)

    pipe = conn.pipeline(True)
    pipe.zcard('idx:' + id)
    if desc:
        pipe.zrevrange('idx:' + id, start, start + num - 1)
    else:
        pipe.zrange('idx:' + id, start, start + num - 1)
    results = pipe.execute()
    return results[0], results[1], id


def _zset_common(conn, method, scores, ttl=30, **kw):
    id = str(uuid.uuid4())
    execute = kw.pop('_execute', True)
    pipe = conn.pipeline(True) if execute else conn
    for key in scores.keys():
        scores['idx:' + key] = scores.pop(key)
    getattr(pipe, method)('idx:' + id, scores, **kw)
    pipe.expire('idx：' + id, ttl)
    if execute:
        pipe.execute()
    return id


def string_to_score(string, ignore_case=False):
    if ignore_case:
        string = string.lower()
    pieces = map(ord, string[:6])
    while len(pieces) < 6:
        pieces.append(-1)
    score = 0
    for piece in pieces:
        score = score * 257 + piece + 1
    return score * 2 + (len(string) > 6)


def zintersect(conn, items, ttl=30, **kw):
    return _zset_common(conn, 'zinterstore', dict(items), ttl, **kw)


def zunion(conn, items, ttl=30, **kw):
    return _zset_common(conn, 'zunionstore', dict(items), ttl, **kw)


def print_hi(name):
    # 在下面的代码行中使用断点来调试脚本。
    print(f'Hi, {name}')  # 按 Ctrl+F8 切换断点。


# 按间距中的绿色按钮以运行脚本。
if __name__ == '__main__':
    info = {'id': 12, 'created': 1234567, 'updated': 1234567}
    mconn.hmset("kb:doc:12", {'id': 12, 'created': 1234567, 'updated': 1234567})
    mconn.hmset("kb:doc:123", {'id': 123, 'created': 1234568, 'updated': 1234568})
    mconn.hmset("kb:doc:1234", {'id': 1234, 'created': 1234569, 'updated': 1234569})
    index_document(mconn, 123,
                   'InfluxDB 2.5 is the platform purpose-built to collect, store, process and visualize time series data. Time series data is a sequence of data points indexed in time order. Data points typically consist of successive measurements made from the same source and are used to track changes over time. Examples of time series data include')

    index_document(mconn, 12,
                   'The InfluxDB UI provides a web-based visual interface for interacting with and managing InfluxDB. The UI is packaged with InfluxDB and runs as part of the InfluxDB service. To access the UI, with InfluxDB running, visit localhost:8086 in your browser.'
                   'Data points typically consist of successive measurements made from the same source and are used to track changes over time. Examples of time series data include')

    index_document(mconn, 1234,
                   'The InfluxDB UI provides a web-based visual interface for interacting with and managing InfluxDB. The UI is packaged with InfluxDB and runs as part of the InfluxDB service. To access the UI, with InfluxDB running, visit localhost:8086 in your browser.'
                   'Data points typically consist of successive measurements made from the same source and are used to track changes over time. Examples of time series data include')
    print(parse_and_search(mconn, 'date +time made -order', 200))
    print(search_and_sort(mconn, 'date +time made -order', num=1))
    print(search_and_zsort(mconn, 'date +time made -order'))
    print_hi('PyCharm')

# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助
